In this lesson
Phase 4 · Lesson 4.5 · Final Lesson of Phase 4

Transfer Learning and Fine-Tuning

How to build powerful AI applications without training from scratch. The technique that makes modern AI accessible to anyone with a laptop and a dataset.

🕑 30 min read 📊 2 visualisations 💻 Keras + Hugging Face

What is Transfer Learning?

Imagine hiring someone to classify satellite images of farmland. They do not need to learn what a plant is from scratch. They already understand shapes, colours, spatial patterns, and visual textures from years of existing experience. You just need to teach them the specific categories relevant to your task. They transfer knowledge from past experience to a new domain.

Transfer learning in deep learning works the same way. A model trained on a large, general dataset builds up rich internal representations: edge detectors, texture recognisers, shape templates, grammar patterns, semantic relationships. When you face a new task with limited data, you start from these learned representations rather than random weights, and only adapt the parts that need to change.

This matters enormously for practical AI work. Training a large model from scratch requires millions of labelled examples and weeks of compute on expensive hardware. Transfer learning lets you achieve competitive results with hundreds or thousands of examples, trained in minutes on a single GPU or even a CPU.

The Real-World Standard

In industry, almost no AI application trains a neural network from scratch. Companies and researchers start from pre-trained models and fine-tune them. The exceptions are organisations with the scale and resources of Google, Meta, or OpenAI, who train foundation models that everyone else then fine-tunes. Understanding transfer learning is therefore one of the most practically useful skills in AI.

Why Transfer Learning Works

The reason transfer learning is effective comes back to the hierarchical nature of neural network representations discussed in Lesson 4.3. The early layers of a deep network learn very general features. The later layers learn increasingly task-specific features.

Research examining the features learned by large vision networks (such as the work by Zeiler and Fergus in 2014 on visualising CNN features, and later work at OpenAI) has consistently found that the early-layer features, curves, edges, colours, textures, are virtually identical whether the network was trained to recognise animals, faces, medical images, or satellite photographs. These features are not specific to the training task. They are fundamental visual primitives that appear across all natural images.

Feature Hierarchy in a Deep Network Layer 1 Early Conv Edges Colours Gradients Very General Layer 2-3 Mid Conv Corners Curves Textures Mostly General Layer 4-5 Deep Conv Shapes Parts Partially Specific Final Conv Layers Objects Faces, Wheels... Task-Specific Head Dense Layers Classifier Replace for New Task PRE-TRAINED AND FROZEN (or fine-tuned) NEWLY TRAINED

Early layers of a pre-trained network detect universal visual primitives. Only the later layers and the classification head are task-specific. In transfer learning, we reuse the early layers and retrain (or replace) the task-specific parts.

The same principle holds for language models. A network pre-trained on billions of words of text develops an understanding of grammar, word meanings, common reasoning patterns, and factual associations. These representations are useful across nearly any language task, whether the new task is sentiment analysis, legal document review, or medical record summarisation.

Two Approaches: Feature Extraction vs Fine-Tuning

When you transfer a pre-trained model to a new task, you have a spectrum of choices about how much of the pre-trained model to keep frozen and how much to update.

Feature Extraction

Freeze all pre-trained weights. Remove the original classification head. Use the pre-trained network purely as a feature extractor: feed your new images through it and collect the activations from the final layer. Then train a simple classifier (logistic regression, SVM, or a small dense layer) on these extracted features.


Pros: Fast, requires little data, no risk of destroying pre-trained knowledge, works on CPU.

Cons: Cannot adapt features to new domain characteristics. Upper bound on accuracy.

Fine-Tuning

Unfreeze some or all pre-trained layers and train the entire network (or the unfrozen portion) on your new dataset, using a very small learning rate. This allows the model to gently adjust the pre-trained representations to better suit your specific domain without catastrophically forgetting what it learned.


Pros: Higher accuracy ceiling, can adapt to domain-specific features.

Cons: Needs more data, slower training, risk of catastrophic forgetting if learning rate is too high.

In practice, the most reliable strategy is a two-phase approach: first do feature extraction for a few epochs to train the new head, then unfreeze the top portion of the base model and fine-tune with a much lower learning rate.

Transfer Learning for Images in Keras

ResNet50 is a 50-layer residual network pre-trained on ImageNet (Kaiming He et al., 2015). It has learned rich visual features from 1.2 million images across 1,000 categories. We can adapt it to classify any new set of images, even with a small dataset.

Python (Keras / TensorFlow)
import tensorflow as tf
from tensorflow.keras import layers, Model, Input
from tensorflow.keras.applications import ResNet50

# ── 1. Load ResNet50 pre-trained on ImageNet ──────────────────────────
#    include_top=False removes the 1000-class classification head
#    input_shape must have 3 channels (RGB); ResNet50 expects 224×224
base_model = ResNet50(
    weights='imagenet',
    include_top=False,
    input_shape=(224, 224, 3)
)

# ── 2. Phase 1: Feature Extraction — freeze all pre-trained weights ───
base_model.trainable = False

# Build new classification head for YOUR task (e.g. 5 flower categories)
inputs   = Input(shape=(224, 224, 3))
# training=False keeps BatchNorm layers in inference mode while frozen
x = base_model(inputs, training=False)
x = layers.GlobalAveragePooling2D()(x)  # flatten feature maps to a vector
x = layers.Dense(256, activation='relu')(x)
x = layers.Dropout(0.3)(x)
outputs  = layers.Dense(5, activation='softmax')(x)  # 5 new classes

model = Model(inputs, outputs)

model.compile(
    optimizer=tf.keras.optimizers.Adam(1e-3),  # standard LR for training head
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

print(f"Trainable parameters (head only): {model.trainable_variables.__len__()}")

# Phase 1: train only the new head
# (replace train_ds and val_ds with your actual datasets)
history1 = model.fit(train_ds, epochs=10, validation_data=val_ds)

# ── 3. Phase 2: Fine-tuning — unfreeze top layers of base model ───────
base_model.trainable = True

# Freeze everything up to a certain layer; fine-tune only the last block
# ResNet50 has 175 layers; unfreeze the top ~35 (the last residual block)
fine_tune_at = 140
for layer in base_model.layers[:fine_tune_at]:
    layer.trainable = False

# CRITICAL: use a much lower learning rate to avoid overwriting pre-trained weights
model.compile(
    optimizer=tf.keras.optimizers.Adam(1e-5),  # 100x lower than Phase 1
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# Phase 2: fine-tune the unfrozen layers
history2 = model.fit(train_ds, epochs=5, validation_data=val_ds)

print(f"\nFinal validation accuracy: {max(history2.history['val_accuracy']):.4f}")
Trainable parameters (head only): 6 (just the 2 dense layers) Epoch 1/10: loss: 1.2134 - accuracy: 0.5840 - val_accuracy: 0.8060 Epoch 5/10: loss: 0.4521 - accuracy: 0.8640 - val_accuracy: 0.9100 Epoch 10/10: loss: 0.3102 - accuracy: 0.9120 - val_accuracy: 0.9360 --- Fine-tuning (Phase 2) --- Epoch 1/5: loss: 0.2987 - accuracy: 0.9230 - val_accuracy: 0.9500 Epoch 5/5: loss: 0.1654 - accuracy: 0.9510 - val_accuracy: 0.9680 Final validation accuracy: 0.9680

Notice how Phase 1 (feature extraction) already reaches 93.6% validation accuracy by only training the new classification head. Phase 2 (fine-tuning the top layers) then pushes this further to 96.8%. Crucially, the learning rate drops from 0.001 in Phase 1 to 0.00001 in Phase 2. Using a high learning rate during fine-tuning would destroy the carefully learned pre-trained weights, a phenomenon called catastrophic forgetting.

Image Preprocessing for Pre-Trained Models

Pre-trained models expect input images in the exact format they were trained on. ResNet50 expects RGB images scaled and normalised in a specific way. Use tf.keras.applications.resnet50.preprocess_input(image) before feeding images into the model. Failure to preprocess correctly is one of the most common causes of poor transfer learning results.

When to Use Each Strategy

The right approach depends on two factors: how much labelled data you have, and how similar your new domain is to the pre-training domain.

Data Available Domain Similarity Recommended Strategy
Very small (<500 examples) Similar to ImageNet / general text Feature extraction only. Train just the classification head. The pre-trained features are already relevant.
Small (500–5,000 examples) Similar to pre-training domain Feature extraction + optional light fine-tuning of top layers with a very low learning rate (e.g. 1e-5).
Moderate (5,000–50,000 examples) Moderately different (e.g. medical images) Two-phase approach: train head, then fine-tune top half of base model. Use early stopping.
Large (>50,000 examples) Very different from pre-training domain Fine-tune the full model, or train from scratch if domain is very specialised (e.g. protein structure, astronomical data).

The Hugging Face Model Hub

For language tasks, the most important resource in modern AI is the Hugging Face Model Hub (huggingface.co/models). Founded in 2016, Hugging Face has built the dominant open-source platform for sharing and using pre-trained models. As of 2024, the hub hosts well over 700,000 model checkpoints, 150,000 datasets, and is used by virtually every AI research lab and technology company.

Pre-Trained Weights

Download weights for BERT, GPT-2, LLaMA, Stable Diffusion, Whisper, and thousands more, each trained on massive data by well-resourced teams.

Unified API

The transformers library provides identical code patterns regardless of whether you use BERT, T5, or CLIP. Switch models by changing one string.

Domain Models

Find models already fine-tuned for your domain: clinical notes, legal text, code, multiple languages. Often these outperform fine-tuning a general model yourself.

The Hugging Face ecosystem also includes the datasets library for loading benchmark datasets, the evaluate library for computing metrics, and the Trainer class that abstracts away the training loop.

NLP Fine-Tuning with Hugging Face

Let us fine-tune DistilBERT, a compressed version of BERT that retains 97% of its language understanding while being 40% smaller and 60% faster (Sanh et al., 2019), on a sentiment classification task.

Install Dependencies

Run once: pip install transformers datasets evaluate torch

Python (Hugging Face Transformers)
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification,
    TrainingArguments,
    Trainer
)
from datasets import load_dataset
import evaluate
import numpy as np

# ── 1. Load dataset and tokenizer ─────────────────────────────────────
# IMDb: 25,000 training reviews labelled positive (1) or negative (0)
dataset   = load_dataset("imdb")
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")

def tokenize_batch(batch):
    return tokenizer(
        batch["text"],
        padding=True,
        truncation=True,
        max_length=512
    )

tokenized = dataset.map(tokenize_batch, batched=True)

# Use a subset to keep training time reasonable (remove for full dataset)
small_train = tokenized["train"].shuffle(seed=42).select(range(2000))
small_eval  = tokenized["test"].select(range(500))

# ── 2. Load pre-trained model with a new classification head ──────────
# num_labels=2 adds a 2-class dense layer on top of DistilBERT
model = AutoModelForSequenceClassification.from_pretrained(
    "distilbert-base-uncased",
    num_labels=2
)

# ── 3. Define evaluation metric ──────────────────────────────────────
accuracy_metric = evaluate.load("accuracy")

def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return accuracy_metric.compute(predictions=predictions, references=labels)

# ── 4. Configure training ─────────────────────────────────────────────
training_args = TrainingArguments(
    output_dir="./sentiment-model",
    num_train_epochs=3,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    learning_rate=2e-5,               # small LR — fine-tuning a BERT model
    weight_decay=0.01,                # L2 regularisation
    evaluation_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
    logging_steps=50,
)

# ── 5. Train ──────────────────────────────────────────────────────────
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=small_train,
    eval_dataset=small_eval,
    compute_metrics=compute_metrics,
)

trainer.train()

results = trainer.evaluate()
print(f"\nEval accuracy: {results['eval_accuracy']:.4f}")

# ── 6. Save for later use ─────────────────────────────────────────────
model.save_pretrained("./sentiment-model/final")
tokenizer.save_pretrained("./sentiment-model/final")
***** Running training ***** Num examples = 2,000 Num Epochs = 3 Total optimization steps = 375 {'loss': 0.6891, 'learning_rate': 2e-05, 'epoch': 0.5} {'loss': 0.4312, 'learning_rate': 1.6e-05, 'epoch': 1.0} {'eval_loss': 0.3241, 'eval_accuracy': 0.8820, 'epoch': 1.0} {'loss': 0.2854, 'learning_rate': 1.2e-05, 'epoch': 2.0} {'eval_loss': 0.2788, 'eval_accuracy': 0.9040, 'epoch': 2.0} {'loss': 0.2103, 'learning_rate': 4e-06, 'epoch': 3.0} {'eval_loss': 0.2614, 'eval_accuracy': 0.9120, 'epoch': 3.0} Training complete. Eval accuracy: 0.9120

With only 2,000 training examples and 3 training epochs, we achieved 91.2% accuracy on sentiment classification. Training a model from scratch on 2,000 examples would likely yield 60-70% at best. The 65 million pre-trained parameters of DistilBERT do the heavy lifting; we only fine-tune them gently with a learning rate of 2×10⁻⁵.

The Importance of a Low Learning Rate

In the Keras image example we used 1e-5 for fine-tuning. Here we use 2e-5. The reason is the same: the pre-trained weights encode valuable knowledge. A large learning rate destroys this knowledge in the first few gradient steps. A small learning rate makes gentle updates that adapt the representations to the new task while preserving the general knowledge. This is the single most important hyperparameter decision in fine-tuning.

Real-World Impact of Transfer Learning

Transfer learning is not just a laboratory technique. It is the mechanism behind some of the most impactful applied AI work of the past decade.

Medical Imaging

In 2017, a team led by Andre Esteva at Stanford published a paper in Nature showing that a CNN pre-trained on ImageNet and fine-tuned on 130,000 skin lesion images could classify skin cancer with accuracy comparable to certified dermatologists. The model had never been specifically designed for medicine. Transfer from natural images was sufficient.

Drug Discovery

Transformer models pre-trained on large protein and molecular sequence databases are now fine-tuned to predict drug-protein interactions, protein structure properties, and the toxicity of candidate compounds. Models like ESM-2 (Meta AI, 2022) pre-trained on 250 million protein sequences from UniProt have become standard starting points.

Low-Resource Languages

Multilingual models like mBERT and XLM-RoBERTa are pre-trained on text from over 100 languages. Fine-tuning on a small labelled dataset in a low-resource language (e.g. Swahili, Tamil, or Welsh) immediately achieves competitive performance, because the model already understands language structure across many languages.

Domain-Specific AI

BioBERT (fine-tuned on biomedical literature), LegalBERT (legal text), FinBERT (financial text), and ClinicalBERT (clinical notes) all start from general BERT weights and are then further pre-trained and fine-tuned on domain data. This specialisation consistently outperforms general models on domain tasks.

One pattern that has emerged consistently in research is the scaling law effect on transfer learning: the larger the pre-trained model and the more data it was trained on, the better it transfers, even to tasks that seem very different from its original training objective. This is part of why organisations continue to invest in training ever-larger foundation models.

What You Can Build with This Knowledge

With the skills from this lesson, you can classify images into custom categories with a few hundred labelled examples, build a text classifier for any sentiment, topic, or intent classification task with a few thousand examples, and create a domain-adapted language model for specialised fields. All of these are real, production-quality applications that organisations deploy today.

Key Takeaways

🎉 Phase 4 Complete

You have finished Neural Networks!

You now understand how artificial neurons compute, how backpropagation trains them, how CNNs see images, how Transformers understand language, and how to apply all of this to your own problems with pre-trained models. That is the core of modern deep learning.

Start Phase 5: AI in the Real World →
Practice Notebook
Run this lesson's code in Google Colab
All examples + challenges · Free GPU included · No setup needed
Open In Colab
Progress
Done with this lesson?
Mark it complete to track your progress.